/**
 * Copyright [2010] Delta Air Lines, Inc. All Rights Reserved Access,
 * Modification, or Use Prohibited Without Express Permission of Delta Air
 * Lines.
 */
package com.delta.rafg.core.database.impl;


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import com.delta.rafg.core.config.ConfigSettings;
import com.delta.rafg.core.database.DataEvent;
import com.delta.rafg.core.database.DataHandlerOptions;
import com.delta.rafg.core.database.DataHandlerOptions.OVERIDE_OPTION;
import com.delta.rafg.core.database.IDataHandler;
import com.delta.rafg.core.database.IDataListener;
import com.delta.rafg.core.exception.RAFGException;
import com.delta.rafg.core.log.RAFGLogger;


/**
 * @author RAFG Development Team (D9127520) This abstract DataHandler implements
 *         the common data handling components. These include the database
 *         connection pooling mechanism.
 */
public abstract class ADataHandler implements IDataHandler {

    /**
     * Holds the value of serialVersionUID property.
     */
    private static final long serialVersionUID = 5155904953522542120L;

    /**
     * Holds the value of PROP_DATASOURCE_CONFIG_PREFIX property.
     */
    public static final String PROP_DATASOURCE_CONFIG_PREFIX =
        "com.delta.rafg.core.database";
    
    /**
     * Connection pool datasource.
     */
    protected DataSource dataSource = null;

    /**
     * Holds the value of sqlConnection property.
     */
    protected Connection sqlConnection = null;

    /**
     * DataHandlerOptions used to configure this dataHandler.
     */
    protected DataHandlerOptions dataHandlerOptions = null;

    /**
     * Holds the value of monitoredSchemas property.
     */
    protected List<String> monitoredSchemas = null;

    /**
     * Holds the value of monitoredTables property.
     */
    protected List<String> monitoredTables = null;

    /**
     * Holds the value of excludedTables property.
     */
    protected List<String> excludedTables = null;

	protected String pollFrequency = "60"; //once an hour
	
	protected POLL_STYLE pollStyle = POLL_STYLE.INTERVAL;
	
	private static final String INTERVAL = "interval";
	private static final String TIMESTAMP = "timestamp";
	
	// Used to only allow one thread to call DriverManager at a time.
	// Some devs were having concurrentmodification exceptions from
	// DriverManager.
	private static Object driverManagerSyncObj = new Object();
    
    /**
     * Holds the value of dataListeners property.
     */
    protected List<IDataListener> dataListeners = new CopyOnWriteArrayList<IDataListener>();
    	
    /*
     * (non-Javadoc)
     * @see com.delta.rafg.core.database.IDataHandler#addDataListener
     * (com.delta.rafg.core.database.IDataListener)
     */
    @Override
    public void addDataListener(final IDataListener dataListener) {

        synchronized (dataListeners) {
            dataListeners.add(dataListener);
        }
    }

    /**
     * This method does to close the database connection.
     */
    @Override
    public void closeConnection() {

        try {
            if (sqlConnection != null) {
                sqlConnection.close();
            }
        } catch (final SQLException e) {
        }
        sqlConnection = null;
    }

    /**
     * This method used to close the DB connection.
     * @see com.delta.rafg.core.database.IDataHandler#destroy()
     */
    @Override
    public void destroy() {

        try {
            closeConnection();
        } catch (final Exception e) {
            // ignore any exceptions for the destroy
        }
    }

    /**
     * This method is used to throw the UnsupportedOperationException.
     * @return String[]
     * @see com.delta.rafg.core.database.IDataHandler#getChangedTables()
     * @throws RAFGException
     */
    @Override
    public String[] getChangedTables() throws RAFGException {

        throw new java.lang.UnsupportedOperationException("getChangedTables()"
            + "not implemented.");
    }

    /**
     * This method is used to get a database connection and initialize conn
     * variable.
     * @return Connection
     * @throws RAFGException
     */
    @Override
    public synchronized Connection getConnection() throws RAFGException {

        try {
            Connection conn;
            if (getDataSource() == null) {
                if (sqlConnection == null || sqlConnection.isClosed()) {
                    final String driverClass =
                        getDataHandlerOptions().getDriverClassName();
                    Class.forName(driverClass);
                    final String url =
                        getDataHandlerOptions().getConnectionUrl();
                    final String user = getDataHandlerOptions().getUsername();
                    final String pass = getDataHandlerOptions().getPassword();
                    synchronized (driverManagerSyncObj) {
                    	sqlConnection =
                    		DriverManager.getConnection(url, user, pass);
                    }
                    RAFGLogger.info("(" + getDataHandlerOptions().getDataHandlerPropertyIdentifier() + ") Connecting to SQL datasource: " + url);
                }
                conn = sqlConnection;
            } else {
                conn = getDataSource().getConnection();
            }
            return conn;
        } catch (final java.sql.SQLException e) {
            throw new RAFGException(e);
        } catch (final ClassNotFoundException e) {
            throw new RAFGException(e);
        }
    }

    /**
     * @return DataHandlerOptions used to initialize this DataHandler.
     */
    @Override
    public DataHandlerOptions getDataHandlerOptions() {

        return dataHandlerOptions;
    }

    /**
     * @return
     */
    protected String getDataHandlerPropertyPrefix() {

        return PROP_DATASOURCE_CONFIG_PREFIX + "."
            + dataHandlerOptions.getDataHandlerPropertyIdentifier();
    }

    /**
     * @return DataSource object for JDNI datasource. Null if DataHandler does
     *         not use a datasource.
     */
    public final DataSource getDataSource() {

        return dataSource;
    }

    /**
     * @return Schemas being monitored for this datasource as defined in
     *         properties.
     */
    @Override
    public String[] getMonitoredSchemas() {

        if (monitoredSchemas != null) {
            return monitoredSchemas
                .toArray(new String[monitoredSchemas.size()]);
        } else {
            return new String[0];
        }
    }

    /**
     * @return Tables being monitored for this datasource as defined in
     *         properties.
     */
    @Override
    public String[] getMonitoredTables() {

        if (monitoredTables != null) {
            return monitoredTables.toArray(new String[monitoredTables.size()]);
        } else {
            return new String[0];
        }
    }

    /**
     * @param fullyQualifiedTableName
     * @return the schema name portion of a fully qualified schema.table
     *         reference
     */
/*    
    public String getSchemaName(final String fullyQualifiedTableName) {

        final String[] tokens =
            fullyQualifiedTableName.split("[.](?=([^\"]*\"[^\"]*\")*[^\"]*$)");
        String retStr = null;
        if (tokens != null && tokens.length > 1) {
            retStr = tokens[0].trim().replace("\"", "");
        }
        return retStr;
    }
*/
    /**
     * @param fullyQualifiedTableName
     * @return the table name portion of a fully qualified schema.table
     *         reference
     */
/*    
    public String getTableName(final String fullyQualifiedTableName) {

        final String[] tokens =
            fullyQualifiedTableName.split("[.](?=([^\"]*\"[^\"]*\")*[^\"]*$)");
        String retStr = null;
        if (tokens != null && tokens.length > 0) {
            retStr = tokens[tokens.length - 1].trim().replace("\"", "");
        }
        return retStr;
    }
*/
    /**
     * This method initialize DataHandlerOptions.
     * @param options DataHandlerOptions
     * @throws RAFGException
     */
    @Override
    public void initialize(final DataHandlerOptions options)
        throws RAFGException {

        if (options == null) { throw new java.lang.IllegalArgumentException(
            "options cannot be null"); }
        dataHandlerOptions = options;
        if (dataHandlerOptions.getConfigSettings() == null) {
            dataHandlerOptions.setConfigSettings(ConfigSettings.getSingleton());
        }
        loadDataHandlerProperties();
        final String jndiName = options.getJndiName();
        if (!dataHandlerOptions.hasOverideOption(OVERIDE_OPTION.IGNORE_JNDI) && jndiName != null && jndiName.length() > 0) {
            Context envCtx;
            try {
                envCtx = (Context) new InitialContext();  //.lookup("java:comp/env");
                dataSource = (DataSource) envCtx.lookup("java:comp/env/jdbc/" + jndiName);
                RAFGLogger.info("(" + getDataHandlerOptions().getDataHandlerPropertyIdentifier() + ") Connected to Container DataSource: " + jndiName);
            } catch (final NamingException e) {
            	RAFGLogger.info(e, "Failed to open " + dataHandlerOptions.getDataHandlerPropertyIdentifier() + " as jndi DataSource.  Falling back to DriverManager method. (" + e.getMessage() + ")", null);
            }
        }
        populateMonitoredSchemas();
        populateMonitoredTables();
    }

    /**
     * This method is used to load and initialize DataHandlerOptions property
     * @param dataHandlerPropertyIdentifier
     * @throws RAFGException
     */
    @Override
    public void initialize(final String dataHandlerPropertyIdentifier)
        throws RAFGException {

        initialize(dataHandlerPropertyIdentifier, ConfigSettings.getSingleton());
    }

    @Override
    public void initialize(final String dataHandlerPropertyIdentifier, final DataHandlerOptions options)
        throws RAFGException {
        dataHandlerOptions = options;
        dataHandlerOptions.setDataHandlerPropertyIdentifier(dataHandlerPropertyIdentifier);
        loadDataHandlerProperties();
        initialize(dataHandlerOptions);
    }

    /**
     * This method is used to load and initialize DataHandlerOptions property
     * @param dataHandlerPropertyIdentifier
     * @param configSettings
     * @throws RAFGException
     */
    @Override
    public void initialize(final String dataHandlerPropertyIdentifier,
        final ConfigSettings configSettings) throws RAFGException {

        if (dataHandlerPropertyIdentifier == null) { throw new java.lang.IllegalArgumentException(
            "dataHandlerPropertyIdentifier cannot be null"); }
        dataHandlerOptions = new DataHandlerOptions();
        dataHandlerOptions.setConfigSettings(configSettings);
        dataHandlerOptions
            .setDataHandlerPropertyIdentifier(dataHandlerPropertyIdentifier);
        loadDataHandlerProperties();
        initialize(dataHandlerOptions);
    }

    /**
     *
     */
    private void loadDataHandlerProperties() {

        final ConfigSettings configSettings =
            dataHandlerOptions.getConfigSettings();
        
        //get rafg environment setting from System if defined. 
        //It should be one of: SystemTest, SystemIntegration, Production
        final String environment = System.getProperty(ConfigSettings.SYSTEM_RAFG_ENVIRONMENT_KEY);

        final String propertyPrefix = getDataHandlerPropertyPrefix();

        String driverclass = configSettings.getProperty(propertyPrefix + ".driverclass");
        String servicename = configSettings.getProperty(propertyPrefix + ".servicename");
        String username = configSettings.getProperty(propertyPrefix + ".username");
        String password = configSettings.getProperty(propertyPrefix + ".password");
        String url = configSettings.getProperty(propertyPrefix + ".url");
        String protocol = configSettings.getProperty(propertyPrefix + ".protocol");
        String jndi = configSettings.getProperty(propertyPrefix + ".jndi");
        String pollfrequency = configSettings.getProperty(propertyPrefix + ".pollfrequency");
        String pollstyle = configSettings.getProperty(propertyPrefix + ".pollstyle");
        String ignoreJndi = configSettings.getProperty(propertyPrefix + ".ignoreJndi");
        
        //try system specific settings, if any don't exist, use what I already got from above.
        if (environment != null) {
        	driverclass = configSettings.getProperty(propertyPrefix + ".driverclass." + environment, driverclass);
            servicename = configSettings.getProperty(propertyPrefix + ".servicename." + environment, servicename);
            username = configSettings.getProperty(propertyPrefix + ".username." + environment, username);
            password = configSettings.getProperty(propertyPrefix + ".password." + environment, password);
            url = configSettings.getProperty(propertyPrefix + ".url." + environment, url);
            protocol = configSettings.getProperty(propertyPrefix + ".protocol." + environment, protocol);
            jndi = configSettings.getProperty(propertyPrefix + ".jndi." + environment, jndi);
            pollfrequency = configSettings.getProperty(propertyPrefix + ".pollfrequency." + environment, pollfrequency);
            pollstyle = configSettings.getProperty(propertyPrefix + ".pollstyle." + environment, pollstyle);
        }
        
        if (driverclass != null) {
            dataHandlerOptions.setDriverClassName(driverclass);
        }
        if (servicename != null) {
            dataHandlerOptions.setServiceName(servicename);
        }
        if (username != null) {
            dataHandlerOptions.setUsername(username);
        }
        if (password != null) {
            dataHandlerOptions.setPassword(password);
        }
        if (url != null) {
            dataHandlerOptions.setConnectionUrl(url);
        }
        if (protocol != null) {
            dataHandlerOptions.setProtocol(protocol);
        }
        if (jndi != null) {
            dataHandlerOptions.setJndiName(jndi);
        }
        if (pollfrequency != null) {
        	pollFrequency = pollfrequency;
        }
        if (pollstyle != null) {
        	if (pollstyle.equalsIgnoreCase(INTERVAL)) {
        		pollStyle = POLL_STYLE.INTERVAL; 
        	} else if (pollstyle.equalsIgnoreCase(TIMESTAMP)) {
        		pollStyle = POLL_STYLE.TIMESTAMP; 
        	}
        }
        if (ignoreJndi != null) {
        	if (Boolean.parseBoolean(ignoreJndi)) {
        		dataHandlerOptions.addOverideOption(OVERIDE_OPTION.IGNORE_JNDI);
        	}
        }
    }

    /**
     * This method is used to initialize monitoredSchemas property.
     */
    private void populateMonitoredSchemas() {

        final DataHandlerOptions opts = getDataHandlerOptions();
        final ConfigSettings props = opts.getConfigSettings();
        // They are all props that start with com.delta.rafg.database.
        final String schemasStr =
            props.getProperty(getDataHandlerPropertyPrefix()
                + ".monitor.schemas");
        if (schemasStr != null) {
            final String[] schemasArray = schemasStr.split(",");
            if (monitoredSchemas == null) {
                monitoredSchemas = new ArrayList<String>();
            }
            for (final String s : schemasArray) {
                monitoredSchemas.add(s.trim());
            }
        }
    }

    /**
     *
     */
    private void populateMonitoredTables() {

        final DataHandlerOptions opts = getDataHandlerOptions();
        final ConfigSettings props = opts.getConfigSettings();
        final String tablesStr =
            props.getProperty(getDataHandlerPropertyPrefix()
                + ".monitor.tables");
        final String excludedTablesStr =
            props.getProperty(getDataHandlerPropertyPrefix()
                + ".monitor.tables.excluded");
        if (excludedTablesStr != null && excludedTablesStr.length() > 0) {
            final String[] excludedTablesArray = excludedTablesStr.split(",");
            if (excludedTables == null) {
                excludedTables = new ArrayList<String>();
            }
            for (final String t : excludedTablesArray) {
                excludedTables.add(t.trim());
            }
        }
        if (tablesStr != null && tablesStr.length() > 0) {
            final String[] tablesArray = tablesStr.split(",");
            if (monitoredTables == null) {
                monitoredTables = new ArrayList<String>();
            }
            for (final String t : tablesArray) {
                monitoredTables.add(t.trim());
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see com.delta.rafg.core.database.IDataHandler#removeDataLister
     * (com.delta.rafg.core.database.IDataListener)
     */
    @Override
    public void removeDataLister(final IDataListener dataListener) {

        synchronized (dataListeners) {
            dataListeners.remove(dataListener);
        }
    }

	public void fireTableChangedEvent(DataEvent dataEvent) {
		for (IDataListener l : dataListeners) {
			String monitoredTables[] = l.getMonitoredTables();
			for (String t : monitoredTables) {
				if (dataEvent.getChangedTable().equals(t)) {
					try {
						l.tableChangeEvent(dataEvent);
					} catch (Exception e) {
						RAFGLogger.error(e, "Caught Exception notifying on table change event: " + dataEvent.toString() + ":" + e.getMessage(), null);
					}
					break;
				}
			}
		}
	}
	
	@Override
	public POLL_STYLE getPollStyle() {
		return pollStyle;
	}

	@Override
	public String getPollFrequency() {
		return pollFrequency;
	}

}
